Skocz do zawartości
  • 👋 Witaj na MPCForum!

    Przeglądasz forum jako gość, co oznacza, że wiele świetnych funkcji jest jeszcze przed Tobą! 😎

    • Pełny dostęp do działów i ukrytych treści
    • Możliwość pisania i odpowiadania w tematach
    • System prywatnych wiadomości
    • Zbieranie reputacji i rozwijanie swojego profilu
    • Członkostwo w jednej z największych społeczności graczy

    👉 Dołączenie zajmie Ci mniej niż minutę – a zyskasz znacznie więcej!

    Zarejestruj się teraz

[TuT] Minecraft w Unity3D


Gość henb

Rekomendowane odpowiedzi

Opublikowano

Jakiekolwiek pytania/problemy/sugestie napisz na PW. Jeśli kilka osób zapyta o to samo, postaram się to wyjaśnić w tym temacie.

Wszystko co tutaj przeczytasz, wszelkie assety, które tutaj dodam podlega pod

DO WHAT THE F**K YOU WANT TO PUBLIC LICENSE
Version 2, December 2004

Copyright © 2004 Sam Hocevar <[email protected]>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

DO WHAT THE F**K YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE F**K YOU WANT TO.




Spis treści

1. Przygotowanie projektu i wstęp do GITa: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/#entry10471086

2. Pierwszy kloc: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/#entry10477052

3. Rzeźbienie kloców: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/#entry10479238

4. Pierwsza optymalizacja : http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/#entry10479915

5. Szum Perlina: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/#entry10480318

5.1 Drobny refactor: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/#entry10491402

6. Więcej chunków: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/?p=10497655

6.1. Druga optymalizacja: http://www.mpcforum.pl/topic/1225314-tutorial-minecraft-w-unity3d/?p=10498523

7. Kolorowanie: http://www.mpcforum.pl/topic/1225314-tut-minecraft-w-unity3d/?p=10554209

8. Drugi refactor i łatanie dziur: 26.07.2014.

Cześć,

Przedstawiam Ci zwycięzcę głosowania na tutorial. (Będę zwracał się do Ciebie w liczbie pojedynczej. Nie jesteś przecież Wami, tylko Tobą. No i nie jesteś w armii radzieckiej).

Dzisiaj chciałbym opowiedzieć o tym jak przygotować się do robienia większego projektu. Żeby nie było tekstów: „No robiłem duży projekt. Oj długo robiłem. Fajny jest. No nie pokażę, bo to na starym komputerze było. Jak była burza to piorun uderzył i sam wiesz… Poszło się paść”. Nie ma nic śmieszniejszego niż takie akcje. Prowadzenie dużego projektu bez repozytorium to jazda na krawędzi. Często się udaje, ale zdarza się że coś nawali. Co wtedy? Co zrobisz? Nic nie zrobisz :D Dodatkowo ułatwi to pracę w zespole. Jeżeli będziesz chciał/a pracować jako developer to nikt się z Tobą nie będzie chciał bawić, a jeszcze bardziej nie będą chcieli z Tobą pracować jak się nie znasz na wersjonowaniu kodu.

Czym jest repozytorium (w skrócie repo)? W dużym uproszczeniu to taki magazyn na kod i assety. Robisz coś dzisiaj, pod koniec pracy wrzucasz to do tego magazynu. Jutro pracujesz nad tym dalej ale w pewnym momencie coś zepsułeś. No i jest nieciekawie. Nie bardzo wiesz co robić, kilka skryptów wywaliłeś, namieszałeś w kodzie. No nie buduje się. Nie ruszy. Wtedy możesz wrócić do stanu z dnia poprzedniego( lub poprzedniego włożenia kodu do magazynu). Masz to co było wczoraj, ale przynajmniej działa. Cały dzień stracony, ale przynajmniej wiesz czego unikać ;-)

Jest wiele serwisów umożliwiających wersjonowanie. Ja wybrałem bitbucket, ponieważ ma opcję darmowego i prywatnego repozytorium.

https://bitbucket.org/

Myślę, że poradzisz sobie z założeniem tam konta. Dodatkowo pobierz aplikację SourceTree.

http://www.sourcetreeapp.com/

W międzyczasie stwórz katalog w którym będziesz trzymać projekt.
11031140393988524822.png
katalog w swoim naturalnym środowisku

Na stronie bitbucket przejdź do tworzenia repo.
88841140393988524822.png

I wypełnij wymagane pola.
46621140393988524822.png


No i niebieski przycisk.

Następnie wybierz Clone in SourceTree
95164140393988524822.png
Twoja przeglądarka powinna zapytać o to, czy może uruchomić SourceTree. Z grzeczności się zgadzamy.


Jako Destination Path wybierz katalog, który przeznaczyłeś na swój projekt.
36013140393988524822.png

Jeżeli wszystko poszło zgodnie z planem powinieneś zobaczyć coś takiego:
59802140393988524822.png

Stwórz nowy projekt w Unity w tym samym katalogu. Możesz stworzyć testową scenę (niech wiedzą, że jednak coś jest w projekcie :D ).
Zapisz, zamknij, nie potrzebujesz na dzisiaj Unity.

Kliknij dwukrotnie to co masz w SourceTree na poprzednim zdjęciu (W moim przypadku Minecraft Unity).

No trochę zaczyna się robić burdel. Zaraz to ogarniesz.
38512140393988524822.png


W oknie „Files in the working tree” są wszystkie pliki, które stworzyło Unity. Nie potrzebujesz ich wszystkich. Do tego służy gitignore. W tym pliku będą wszystkie rozszerzenia, które nie są potrzebne. Jak to zrobić? Najłatwiej jest kliknąć prawym na dowolny plik i wybrać Ignore. Zaznacz Ignore all files with this extension.
50397140393988624822.png

Stworzyłeś właśnie gitignore. Kliknij go dwukrotnie. Przejdziesz wtedy do edycji pliku.
95147140393988624822.png
Zmień jego treść na gitignore dla unity, który jest poniżej.

# =============== #
# Unity generated #
# =============== #
Temp/
Obj/
UnityGenerated/
Library/

# ===================================== #
# Visual Studio / MonoDevelop generated #
# ===================================== #
ExportedObj/
*.svd
*.userprefs
*.csproj
*.pidb
*.suo
*.sln
*.user
*.unityproj
*.booproj

# ============ #
# OS generated #
# ============ #
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db

94929140393988624822.png
Wyłącz i jeżeli system zapyta Cię o zapis to wiesz co robić ;-)

Jest już porządek.
79072140393988624822.png

Zaznacz te pliki i naciśnij Add na samej górze.
91482140393988624822.png
(nie przejmuj się tooltipem, miałem akurat kursor nad stash).

Kiedy dodałeś pliki, naciśnij Commit.
25974140393988624822.png
możesz nazwać go jak chcesz, ja zazwyczaj pierwszy commit nazywam initial commit


94734140393988624822.png
Jeżeli commit się udał, wybierz Push.

Teraz przygotuj nowy branch. Wybierz (tutaj element zaskoczenia) Branch.
75524140393988624822.png

Nazwij go jakkolwiek. Ja będę nazywał je numerami tutoriali, które będę publikował.
86696140393988724822.png
Następnie create branch.

To wszystko na dzisiaj. Mam nadzieję, że się nie zniechęciłeś. Nie obiecuję fajerwerków i wybuchów przez cały czas prowadzenia tutoriala ale mam nadzieję że końcowy efekt Ci się spodoba.


Jutro premiera drugiej części, w której postawisz swojego pierwszego kloca.

Mam jeszcze prośbę do tych, którzy mają jakiś pomysł jak coś mogę ulepszyć, uprościć. Jeżeli znajdziecie jakiś błąd bardzo chętnie się o nim dowiem. Jeżeli jeszcze jesteś w stanie podpowiedzieć jak ten błąd wyeliminować to będę wdzięczny (o ile ten błąd nie jest brakiem kropki na końcu komentarza w kodzie).
I ostatnia informacja przed faktycznym rozpoczęciem kodzenia w Unity. Raczej niechętnie będę realizował prośby w stylu: "Dodaj jeszcze samolot do tej gry bo będzie fajnie". Niestety taki ficzer interesuje tylko osobę, która to napisała. Ja skupię się na tym jak voxelowy świat jest tworzony.

edit: Odpuszczę już Unity Test Tools, ale kiedyś o nich opowiem :)
60442140393988824822.png

Opublikowano

Uwaga: W przeciwieństwie do mini tutoriali, w tym będę używał języka angielskiego podczas pisania skryptów. Ciężko znaleźć mi tłumaczenia, które będą się sprawdzały w kodzie tak jak chunk, mesh itp.

 

 

Stawianie pierwszego kloca. (który pod koniec tej części tutoriala i tak zniknie :( )

 

Zacznij od zaimportowania tego character controllera, który bardziej Ci się podoba. Ja wybrałem First Person Controller. Do tego dołóż swój ulubiony skybox, żeby nie było tak smutno. Dodatkowo dodaj na scenę directional light, cube i ustaw character controller nad nim. 

 

Żeby nie robić burdelu w projekcie, stwórz folder na skrypty. Pierwszym skryptem będzie World.

W nim znajdują się trzy zmienne. Dwie z nich odpowiadają za rozmiar chunka. Grałeś w Minecrafta, więc wiesz co to chunk, bo często się nie ładował ;)

Trzecia przechowuje seed, z którego będziesz generował później teren.

	public int ch_width = 20;
	public int ch_height = 20;
	public int randomSeed = 0;

Ponieważ często będę odwoływał się do tego skryptu postanowiłem zrobić jeszcze to:

public static World activeWorld;

To tyle zmiennych w tym skrypcie.

 

Usuń Update(), a Start() zmień na Awake().

Następnie ustaw activeWorld w ten sposób:

activeWorld = this;

this odwołuje się do tego konkretnego skryptu. Przykładowo this.gameObject odwołuje się do gameObjectu do którego przypięty jest ten skrypt. Myślę, że ten temat wyczerpałem.

Następnym krokiem będzie przypisanie losowej wartości do seeda.

if(randomSeed ==0)
     randomSeed = Random.Range(0, int.MaxValue);

Przy pierwszym uruchomieniu danego świata, randomSeed ma wartość 0. Wtedy losuję dla niego wartość w zakresie dodatnich integerów. 

 

Dodaj ten skrypt do kamery. Kiedyś opowiem o DAO. Na chwilę obecną kamera wystarczy.

 

 

Następny skrypt to Chunk.

Ponieważ w tym skrypcie korzystam z list, dodaj na początku:

using System.Collections.Generic; 

W Chunk.cs będą operacje na MeshColliderze, rendererze i filtrze. ( OMUJBORZE jak to brzmi odmienione)

Żeby mieć pewność, że te komponenty występują na danym obiekcie skorzystam z RequireComponent. Dodaje się to nad nazwą klasy.

[RequireComponent (typeof(MeshCollider))]
[RequireComponent (typeof(MeshRenderer))]
[RequireComponent (typeof(MeshFilter))] 

W tym momencie, po dodaniu skryptu na dowolny obiekt Unity doda te komponenty do obiektu (o ile wcześniej ich tam nie było).

 

Zmienne, których używam w tym skrypcie to:

public Mesh v_Mesh;
protected MeshCollider meshCollider;
protected MeshRenderer meshRenderer;
protected MeshFilter meshFilter;
public byte [,,] map; 

W skrócie opowiem za co odpowiada Mesh, MeshCollider, MeshRenderer i MeshFilter.

Mesh to w dużym uproszczeniu "skorupka" obiektów w 3D. MeshCollider odpowiada za kolizje (jak nietrudno się domyśleć). MeshFilter natomiast przekazuje je do MeshRenderera. Ten z kolei wyświetla to co dostał tam, gdzie dany GameObject się znajduje.

Na samym końcu jest 3-wymiarowa tablica bajtów. W niej będę przechowywać id klocków, które się tam znajdą.

 

Ten skrypt wrzuć na wcześniej dodany na scenę cube.

 

Przejdźmy do inicjalizacji. Do void Start() dodaj:

meshCollider = GetComponent<MeshCollider>();
meshRenderer = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>(); 

Następnie trzeba ustalić rozmiar naszej 3-wymiarowej tablicy. Ponieważ cała inicjalizacja w Chunk.cs dzieje się w Start(), a w World.cs odbywa się w Awake() to możemy korzystać już z wartości w World.

map = new byte[World.activeWorld.ch_width, World.activeWorld.ch_width, World.activeWorld.ch_height];

W pseudokodzie: SkryptSwiata.(zmienna static z tego skryptu).szerokosc chunka.

Po ludzku: [szerokość, szerokość, wysokość] ustalona w World.cs.

 

Teraz trzeba potrzebna jest iteracja po tej tablicy. Wstępnie określę ją jako "po szerokośći" i "po głębokości". Czyli po osi x oraz z.

for(int x = 0; x < World.activeWorld.ch_width; x++)
{
        for (int z = 0; z < World.activeWorld.ch_width; z++)
	{
		map[x, 0, z] = 1;
		map[x, 1, z] = (byte)Random.Range (0,1);
	}
} 

Dla dolnej warstwy przypisuję wartość 1. Czyli tak zwane "jest klocek".

Dla wyższej warstwy przypisuję losowo 0 lub 1. Nie zapomnij o rzutowaniu na byte, ponieważ taki typ trzymasz w tablicy. Random.Range w Unity losuje float albo int. 

Nie martw się. Generowanie świata w tym tutorialu nie zostawię opartego o Random.Range. Skorzystam z szumu Perlina. O tym kiedy indziej.

Wracając do tematu dzisiejszego tutoriala: na samym końcu wywołaj metodę GenerateMesh(), którą za chwilę stworzysz.

 

 

GenerateMesh:

public virtual void GenerateMesh() 

virtual, ponieważ będziesz ją wywoływał wielokrotnie i na wiele sposobów.

W tej metodzie ustawiam wartości dla v_Mesh.

v_Mesh = new Mesh(); 

A także korzystam z trzech list. Jedna z nich odpowiada za vertexy (czyli pozycje węzłów), miejsca na które będę wrzucał teksturę, oraz trójkąty z których będę budował Mesh. Dla początkującego brzmi to strasznie, ale w następnym tutorialu wytłumaczę dokładniej z czego składa się mesh. Dzisiaj zajmę się przygotowaniem danych tak, aby później można było na ich podstawie budować voxele.

Oto te listy:

	List<Vector3> verts = new List<Vector3>();
	List<Vector2> UVs = new List<Vector2>();
	List<int> tris = new List<int>(); 

Następną rzeczą jest iteracja przez cały chunk. Dzisiaj będą tylko pętle, które później będę wypełniał. Tak jak wcześniej szerokość z głębokością, a dodatkowo wysokość.

	for(int x = 0; x < World.activeWorld.ch_width; x++)
	{
		for(int y = 0; y < World.activeWorld.ch_height; y++)
		{
			for (int z = 0; z < World.activeWorld.ch_width; z++)
			{

			}
		}
	} 

Kiedy już wyjdziesz z tych pętli trzeba coś zrobić z v_Mesh. Ustawić vertices, uv i trójkąty. 

		v_Mesh.vertices = verts.ToArray();
		v_Mesh.uv = UVs.ToArray();
		v_Mesh.triangles = tris.ToArray(); 

Z racji tego, że w tej metodzie verts, uv i triangles mam w listach - muszę wrzucić je do tablicy które przyjmuje Mesh. Robię to za pomocą ToArray();

Po operacjach na vertexach trzeba wywołać metody RecalculateBounds i RecalculateNormals, żeby mieć pewność że to co liczyliśmy ma swoje odzwierciedlenie na scenie.

 

 Dwie ostatnie rzeczy w skryptowaniu na dzisiaj to przypisanie stworzonego Mesha do meshFilter i meshCollider.

		meshFilter.mesh = v_Mesh;
		meshCollider.sharedMesh = v_Mesh;

Kiedy uruchomisz symulację zauważysz, że cube znika. Kiedy zaznaczysz go w hierarchii to widać tylko collider. Dlaczego? Ponieważ pętle w GenerateMesh są puste. Bez żadnego vertexa i trójkąta nie da się zbudować mesha. Tym zajmę się w następnej części tutoriala.

 

 

 

Całość World.cs

using UnityEngine;
using System.Collections;

public class World : MonoBehaviour {

	public int ch_width = 20;
	public int ch_height = 20;
	public int randomSeed = 0;

	public static World activeWorld;

	// inicjalizacja
	void Awake () {
		activeWorld = this;
		if(randomSeed == 0)
			randomSeed = Random.Range(0, int.MaxValue);
	}

} 

Całość Chunk.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent (typeof(MeshCollider))]
[RequireComponent (typeof(MeshRenderer))]
[RequireComponent (typeof(MeshFilter))]
public class Chunk : MonoBehaviour {


	public Mesh v_Mesh;
	protected MeshCollider meshCollider;
	protected MeshRenderer meshRenderer;
	protected MeshFilter meshFilter;
	public byte [,,] map;

	// Use this for initialization
	void Start () {

		meshCollider = GetComponent<MeshCollider>();
		meshRenderer = GetComponent<MeshRenderer>();
		meshFilter = GetComponent<MeshFilter>();

		map = new byte[World.activeWorld.ch_width, World.activeWorld.ch_width, World.activeWorld.ch_height];

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			for (int z = 0; z < World.activeWorld.ch_width; z++)
			{
				map[x, 0, z] = 1;
				map[x, 1, z] = (byte)Random.Range (0,1);
			}
		}
		GenerateMesh();
	}

	public virtual void GenerateMesh() {
		v_Mesh = new Mesh();

		List<Vector3> verts = new List<Vector3>();
		List<Vector2> UVs = new List<Vector2>();
		List<int> tris = new List<int>();

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			for(int y = 0; y < World.activeWorld.ch_height; y++)
			{
				for (int z = 0; z < World.activeWorld.ch_width; z++)
				{

				}
			}
		}

		v_Mesh.vertices = verts.ToArray();
		v_Mesh.uv = UVs.ToArray();
		v_Mesh.triangles = tris.ToArray();
		v_Mesh.RecalculateBounds();
		v_Mesh.RecalculateNormals();

		meshFilter.mesh = v_Mesh;
		meshCollider.sharedMesh = v_Mesh;


	}

}

 

 

Nie pozostało Ci dzisiaj nic innego jak zapisanie skryptów i sceny. Add, Commit i Push na mastera.

Opublikowano

Rzeźbienie kloców.

 

W tej części poradnika dodam kilka ważnych rzeczy do Chunk.cs. Chodzi o uzupełnienie pustej pętli z poprzedniego posta i nową metodę dzięki której wyświetlam boki klocków.

 

Zacznę od metody GenerateFace.

	public virtual void GenerateFace(byte block, Vector3 corner, Vector3 up, Vector3 right, bool rev, List<Vector3> verts, List<Vector2> uvs, List<int> tris)

Jej parametry to: id klocka, miejsce w którym zaczynam generować bok, wektor skierowany w górę i w prawo, bool ( opowiem o nim niżej), listę węzłów, listę uv oraz listę trójkątów.

bool rev odpowiada za to, czy bok jest "odwrócony". Co to znaczy odwrócony? Otóż:

Trójkąt to najmniejsza figura, którą jesteśmy w stanie wyświetlić. Kwadrat można podzielić na dwa trójkąty. Trójkątów nie ma sensu już dzielić.

06GMIGS.png

Na tym obrazku mam kwadrat. Jak widzisz składa się z 2 trójkątów. Pierwszy z nich ma indeksy wierzchołków 0,1,2. Drugi z nich ma 2,3,0. Zauważ że są podawane zgodnie z ruchem wskazówek zegara. Jeżeli bool rev ma wartośc false - ten kwadrat będzie składał się z trójkątów z indeksami 1,0,2 oraz 3,2,0.

 

W tej metodzie mam zmienną typu int, która wynosi tyle ile liczba obiektów w liście verts.

int index = verts.Count;

Do tej tablicy dodaję następnie te Vector3, które przekazałem do funkcji.

	verts.Add (corner);
	verts.Add (corner + up);
	verts.Add (corner + up + right);
	verts.Add (corner + right);

Zauważ, że tutaj też magia dzieje się zgodnie z ruchem wskazówek zegara.

 

Kolejną rzeczą jest dodanie koordynatów do listy uvs.

	uvs.Add (new Vector2(0,0));
	uvs.Add (new Vector2(0,1));
	uvs.Add (new Vector2(1,1));
	uvs.Add (new Vector2(1,0));

Następnie, jeżeli rev ma wartość true

		if(rev)
		{

dodaję elementy do tablicy tris. Dodaję index i to o czym mówiłem pod obrazkiem. Wierzchołki trójkątów.

			tris.Add (index + 0);
			tris.Add (index + 1);
			tris.Add (index + 2);
			tris.Add (index + 2);
			tris.Add (index + 3);
			tris.Add (index + 0);

A jeśli nie...

		} else {

to dodaję odwróconą kolejność dla każdego trójkąta

			tris.Add (index + 1);
			tris.Add (index + 0);
			tris.Add (index + 2);
			tris.Add (index + 3);
			tris.Add (index + 2);
			tris.Add (index + 0);

Metoda jest już gotowa do tego, aby z niej korzystać.

Wróć teraz to pustej pętli, która została pod koniec poprzedniego posta.

 

Jeżeli id klocka w danym miejscu to 0 (czyli nie ma tam nic) to nie ma co generować i jedziesz dalej:

	if(map[x, y, z] == 0)
		continue;

W przeciwnym wypadku, jeżeli coś tam jest to:

		byte block = map[x,y,z];

zmienna typu byte ma wartość tego, co jest w tablicy.

 

Stworzę teraz pierwszą parę boków klocka.

//bok 1
GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.forward, false, verts, UVs, tris);
GenerateFace(block, new Vector3(x + 1,y,z), Vector3.up, Vector3.forward, true, verts, UVs, tris);

Przekazuję do metody GenerateFace id klocka, Vector3 z aktualnym położeniem, wektor skierowany w górę oraz w przód. Pierwsza seria będzie odwrócona, druga nie. Dodakowo verts, UVs i tris.

 

W tym momencie, jeżeli wszystko poszło zgodnie z planem, odpal symulację. Zobaczysz wygenerowane boczne ściany klocków.

 

Teraz wygeneruję drugą parę boków.

//bok 2
GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.right, true, verts, UVs, tris);
GenerateFace(block, new Vector3(x,y,z + 1), Vector3.up, Vector3.right, false, verts, UVs, tris);

Oraz "sufit i podłogę" klocka

// gora
GenerateFace(block, new Vector3(x,y,z), Vector3.forward, Vector3.right, false, verts, UVs, tris);
GenerateFace(block, new Vector3(x, y + 1, z), Vector3.forward, Vector3.right, true, verts, UVs, tris);

Play!

 

W zależności od tego jak się nastawiłeś: O! coś się stworzyło / Meh... większy cube.

Its-something.jpg

Stworzyłeś 20x20 klocków. Jeżeli dobrze się rozejrzysz to zauważysz, że są także generowane ściany "wewnątrz" chunka.

MzRkoOV.png

Nie jest to najlepsze rozwiązanie, ale jak się tego pozbyć opowiem w następnej części.

 

 

Chunk.cs po zmianach:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent (typeof(MeshCollider))]
[RequireComponent (typeof(MeshRenderer))]
[RequireComponent (typeof(MeshFilter))]
public class Chunk : MonoBehaviour {


	public Mesh v_Mesh;
	protected MeshCollider meshCollider;
	protected MeshRenderer meshRenderer;
	protected MeshFilter meshFilter;
	public byte [,,] map;

	// Use this for initialization
	void Start () {

		meshCollider = GetComponent<MeshCollider>();
		meshRenderer = GetComponent<MeshRenderer>();
		meshFilter = GetComponent<MeshFilter>();

		map = new byte[World.activeWorld.ch_width, World.activeWorld.ch_width, World.activeWorld.ch_height];

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			for (int z = 0; z < World.activeWorld.ch_width; z++)
			{
				map[x, 0, z] = 1;
				map[x, 1, z] = (byte)Random.Range (0,1);
			}
		}
		GenerateMesh();
	}

	public virtual void GenerateMesh() {
		v_Mesh = new Mesh();

		List<Vector3> verts = new List<Vector3>();
		List<Vector2> UVs = new List<Vector2>();
		List<int> tris = new List<int>();

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			for(int y = 0; y < World.activeWorld.ch_height; y++)
			{
				for (int z = 0; z < World.activeWorld.ch_width; z++)
				{
					if(map[x, y, z] == 0)
						continue;
					byte block = map[x,y,z];

					//bok 1
					GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.forward, false, verts, UVs, tris);
					GenerateFace(block, new Vector3(x + 1,y,z), Vector3.up, Vector3.forward, true, verts, UVs, tris);

					//bok 2
					GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.right, true, verts, UVs, tris);
					GenerateFace(block, new Vector3(x,y,z + 1), Vector3.up, Vector3.right, false, verts, UVs, tris);

					// gora
					GenerateFace(block, new Vector3(x,y,z), Vector3.forward, Vector3.right, false, verts, UVs, tris);
					GenerateFace(block, new Vector3(x, y + 1, z), Vector3.forward, Vector3.right, true, verts, UVs, tris);
				}
			}
		}

		v_Mesh.vertices = verts.ToArray();
		v_Mesh.uv = UVs.ToArray();
		v_Mesh.triangles = tris.ToArray();
		v_Mesh.RecalculateBounds();
		v_Mesh.RecalculateNormals();

		meshFilter.mesh = v_Mesh;
		meshCollider.sharedMesh = v_Mesh;
	}

	public virtual void GenerateFace(byte block, Vector3 corner, Vector3 up, Vector3 right, bool rev, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
	{
		int index = verts.Count;
		verts.Add (corner);
		verts.Add (corner + up);
		verts.Add (corner + up + right);
		verts.Add (corner + right);

		uvs.Add (new Vector2(0,0));
		uvs.Add (new Vector2(0,1));
		uvs.Add (new Vector2(1,1));
		uvs.Add (new Vector2(1,0));

		if(rev)
		{
			tris.Add (index + 0);
			tris.Add (index + 1);
			tris.Add (index + 2);
			tris.Add (index + 2);
			tris.Add (index + 3);
			tris.Add (index + 0);
		} else {
			tris.Add (index + 1);
			tris.Add (index + 0);
			tris.Add (index + 2);
			tris.Add (index + 3);
			tris.Add (index + 2);
			tris.Add (index + 0);
		}
	}

}

 

 

Opublikowano

Pierwsza optymalizacja.

 

W poprzednim wpisie zostawiłem brzydkie i pamięciożerne ściany wewnątrz chunka.

Teraz to naprawię. 

 

Pierwszą metodę, którą dodam do klasy Chunk jest GetBlock.

Najzwyczajniej w świecie zwróci klocek.

	public virtual byte GetBlock(int x, int y, int z)
	{
		if ( 
		    	(x >= World.activeWorld.ch_width) ||
		    	(y >= World.activeWorld.ch_height) ||
		    	(z >= World.activeWorld.ch_width) ||
		    	( x < 0) || ( y < 0) || (z < 0)
		    )
			return 0;
		return map[x,y,z];
	}

Ponieważ sprawa dotyczy przestrzeni 3-wymiarowej, jako parametry podaję współrzędne klocka o który pytam.

W tej metodzie sprawdzam najpierw czy nie wychodzę ze współrzędnymi poza ramy chunka. Jeśli tak to zwracam 0. W przeciwnym wypadku zwracam to co jest w tablicy map.

 

Druga metoda, mówi czy miejsce o które pytam jest widoczne. Tak jak w poprzedniej metodzie jako parametry podaję x, y oraz z.

	public virtual bool Visible (int x, int y, int z)
	{
		byte block = GetBlock(x,y,z);
		switch (block)
		{
		default:
		case 0:
			return true;
		case 1:
			return false;
		}
	}

Chciałbym zaznaczyć, że jest to rozwiązanie tymczasowe. Sprawdzam tylko, czy w danym miejscu jest kloc. 0 - nie ma, 1 -jest.

Zanim to zrobię do zmiennej typu byte przypisuję to co zwróciła poprzednia metoda.

 

Wróć teraz do trzech pętli, w których rysowałeś ściany.

Trzeba je lekko zmodyfikować.

Każdy GenerateFace leci bez względu na to co się dzieje. Wypadałoby to ograniczyć. Z pomocą przyjdzie metoda Visible.

	// lewa sciana
	if( Visible (x - 1, y, z))
		GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.forward, false, verts, UVs, tris);

Najpierw lewa ściana. Sprawdzam czy na lewo od tego miejsca coś jest. Jeżeli nic nie ma to rysuję.

 

 

Teraz prawa ściana:

	// prawa sciana
	if( Visible (x + 1, y, z))
		GenerateFace(block, new Vector3(x + 1,y,z), Vector3.up, Vector3.forward, true, verts, UVs, tris);

Podobnie robię z pozostałymi ścianami. Schemat jest prosty. Jeżeli chodzi o sufit, sprawdzam czy coś jest wyżej (czyli y + 1), podłoga to y - 1 itd.

	// tyl
	if ( Visible (x, y, z - 1))
		GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.right, true, verts, UVs, tris);

	// przod
	if ( Visible (x, y, z + 1))
		GenerateFace(block, new Vector3(x,y,z + 1), Vector3.up, Vector3.right, false, verts, UVs, tris);

	// podloga
	if ( Visible (x, y - 1, z))
		GenerateFace(block, new Vector3(x,y,z), Vector3.forward, Vector3.right, false, verts, UVs, tris);

	// sufit
	if ( Visible (x, y + 1, z))
		GenerateFace(block, new Vector3(x, y + 1, z), Vector3.forward, Vector3.right, true, verts, UVs, tris);

Wewnętrzne ściany zniknęły.

83zWRrL.png

 

 

Na sam koniec w ramach nagrody, że dotarłeś tak daleko przejdź do miejsca w Start(), gdzie:

map[x, 1, z] = (byte)Random.Range (0,1);

i zamień na:

map[x, 1, z] = (byte)Random.Range (0,2);

To doda dodatkową, pseudolosową warstwę klocków nad tym co już zrobiłeś.

yHtQrAo.png

Oto Twój pierwszy losowo wygenerowany chunk.

 

 

 

Chunk.cs update.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[RequireComponent (typeof(MeshCollider))]
[RequireComponent (typeof(MeshRenderer))]
[RequireComponent (typeof(MeshFilter))]
public class Chunk : MonoBehaviour {


	public Mesh v_Mesh;
	protected MeshCollider meshCollider;
	protected MeshRenderer meshRenderer;
	protected MeshFilter meshFilter;
	public byte [,,] map;

	// Use this for initialization
	void Start () {

		meshCollider = GetComponent<MeshCollider>();
		meshRenderer = GetComponent<MeshRenderer>();
		meshFilter = GetComponent<MeshFilter>();

		map = new byte[World.activeWorld.ch_width, World.activeWorld.ch_width, World.activeWorld.ch_height];

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			for (int z = 0; z < World.activeWorld.ch_width; z++)
			{
				map[x, 0, z] = 1;
				map[x, 1, z] = (byte)Random.Range (0,2);
			}
		}
		GenerateMesh();
	}

	public virtual void GenerateMesh() {
		v_Mesh = new Mesh();

		List<Vector3> verts = new List<Vector3>();
		List<Vector2> UVs = new List<Vector2>();
		List<int> tris = new List<int>();

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			for(int y = 0; y < World.activeWorld.ch_height; y++)
			{
				for (int z = 0; z < World.activeWorld.ch_width; z++)
				{
					if(map[x, y, z] == 0)
						continue;
					byte block = map[x,y,z];

					// lewa sciana
					if( Visible (x - 1, y, z))
						GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.forward, false, verts, UVs, tris);

					// prawa sciana
					if( Visible (x + 1, y, z))
						GenerateFace(block, new Vector3(x + 1,y,z), Vector3.up, Vector3.forward, true, verts, UVs, tris);

					// tyl
					if ( Visible (x, y, z - 1))
						GenerateFace(block, new Vector3(x,y,z), Vector3.up, Vector3.right, true, verts, UVs, tris);

					// przod
					if ( Visible (x, y, z + 1))
						GenerateFace(block, new Vector3(x,y,z + 1), Vector3.up, Vector3.right, false, verts, UVs, tris);

					// podloga
					if ( Visible (x, y - 1, z))
						GenerateFace(block, new Vector3(x,y,z), Vector3.forward, Vector3.right, false, verts, UVs, tris);

					// sufit
					if ( Visible (x, y + 1, z))
						GenerateFace(block, new Vector3(x, y + 1, z), Vector3.forward, Vector3.right, true, verts, UVs, tris);
				}
			}
		}

		v_Mesh.vertices = verts.ToArray();
		v_Mesh.uv = UVs.ToArray();
		v_Mesh.triangles = tris.ToArray();
		v_Mesh.RecalculateBounds();
		v_Mesh.RecalculateNormals();

		meshFilter.mesh = v_Mesh;
		meshCollider.sharedMesh = v_Mesh;
	}

	public virtual void GenerateFace(byte block, Vector3 corner, Vector3 up, Vector3 right, bool rev, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
	{
		int index = verts.Count;
		verts.Add (corner);
		verts.Add (corner + up);
		verts.Add (corner + up + right);
		verts.Add (corner + right);

		uvs.Add (new Vector2(0,0));
		uvs.Add (new Vector2(0,1));
		uvs.Add (new Vector2(1,1));
		uvs.Add (new Vector2(1,0));

		if(rev)
		{
			tris.Add (index + 0);
			tris.Add (index + 1);
			tris.Add (index + 2);
			tris.Add (index + 2);
			tris.Add (index + 3);
			tris.Add (index + 0);
		} else {
			tris.Add (index + 1);
			tris.Add (index + 0);
			tris.Add (index + 2);
			tris.Add (index + 3);
			tris.Add (index + 2);
			tris.Add (index + 0);
		}
	}

	public virtual bool Visible (int x, int y, int z)
	{
		byte block = GetBlock(x,y,z);
		switch (block)
		{
		default:
		case 0:
			return true;
		case 1:
			return false;
		}
	}

	public virtual byte GetBlock(int x, int y, int z)
	{
		if ( 
		    	(x >= World.activeWorld.ch_width) ||
		    	(y >= World.activeWorld.ch_height) ||
		    	(z >= World.activeWorld.ch_width) ||
		    	( x < 0) || ( y < 0) || (z < 0)
		    )
			return 0;
		return map[x,y,z];
	}
}
 

 

 

Opublikowano

5. Szum Perlina.

 

Nie mam serca zostawić Cię z takimi nudami jakie wyszły na koniec czwartej części. 

Pokażę Ci jak generować teren za pomocą szumu Perlina. O samym sposobie generowania szumu nie będę opowiadał, bo za słaby na to jestem ;-) Powiem tyle, że wymyślił go profesor Ken Perlin z uniwersytetu w Nowym Jorku. Teraz już wiesz dlaczego jestem za słaby na omawianie samego szumu. 

Nie będę korzystał z szumu, który jest dostępny w Unity ponieważ działa tylko w 2 wymiarach.

Do projektu dodam Noise.cs z tej paczki (która jest dostępna na zasadach Public Domain) :

https://code.google.com/p/simplexnoise/downloads/detail?name=simplexnoise_1_0.zip

Z tego zipa interesuje nas tylko Noise.cs. Wrzuć go do folderu ze skryptami, a następnie otwórz Chunk.cs.

 

Na samej górze dodaj:

using SimplexNoise;

aby mieć dostęp do tego co przed chwilą wrzuciłeś do projektu.

 

Przejdź do void Start() i zmodyfikuj pętle które tam są w taki sposób, aby obejmowały 3 wymiary.

for(int x = 0; x < World.activeWorld.ch_width; x++)
{

	for (int y = 0; y < World.activeWorld.ch_height; y++)
	{

		for (int z = 0; z < World.activeWorld.ch_width; z++)
		{

		}
	}
}

Następną rzeczą jest dodanie trzech zmiennych typu float, które przekażesz do generatora szumu.

W każdej pętli stwórz nową zmienną, zrób rzutowanie na float zmiennej iteracyjnej.

for(int x = 0; x < World.activeWorld.ch_width; x++)
{
	float perlinX = (float)x;
	for (int y = 0; y < World.activeWorld.ch_height; y++)
	{
		float perlinY = (float)y;
		for (int z = 0; z < World.activeWorld.ch_width; z++)
		{
			float perlinZ = (float)z;

		}
	}
}

poniżej zmiennej perlinZ stwórz kolejny float, którego wartość da Ci generator.

float perlin = Noise.Generate(perlinX,perlinY,perlinZ);

Jeżeli wartość tej zmiennej jest większa od 0,2 (Tak jest w moim przypadku. Zachęcam Cię do modyfikowania wartości, które wpisuję "z buta" o ile to nie są zmienne sterujące pętlami. Wtedy możesz uzyskać ciekawszy efekt niż zwykłe pagórki).

if( perlin > 0.2f)
	map[x,y,z] = 1;

To jednak dalej za mało. Ja postanowiłem dzielić zmienne z każdej pętli przez 15. Dodatkowo przed sprawdzeniem wartości szumu zrobiłem tak:

perlin += (10f - (float)y) / 10;

Ostatecznie moja metoda Start() wygląda w ten sposób:

	void Start () {

		meshCollider = GetComponent<MeshCollider>();
		meshRenderer = GetComponent<MeshRenderer>();
		meshFilter = GetComponent<MeshFilter>();

		map = new byte[World.activeWorld.ch_width, World.activeWorld.ch_width, World.activeWorld.ch_height];

		for(int x = 0; x < World.activeWorld.ch_width; x++)
		{
			float perlinX = (float)x / 15;
			for (int y = 0; y < World.activeWorld.ch_height; y++)
			{
				float perlinY = (float)y / 15;
				for (int z = 0; z < World.activeWorld.ch_width; z++)
				{
					float perlinZ = (float)z / 15;
					float perlin = Noise.Generate(perlinX,perlinY,perlinZ);

					perlin += (10f - (float)y) / 10;

					if( perlin > 0.2f)
						map[x,y,z] = 1;
				}
			}
		}
		GenerateMesh();
	}

Zmieniłem testowo rozmiar chunka w World.cs przypiętego do kamery na 50 szerokości i 50 wysokości.

4cUoEzr.png

Naszym jedynym aktualnym zmartwieniem jest to, że generuję tylko jeden typ bloku. To jest po prostu klocek. Nie dirt, kamienie czy złoto. Do tego trzeba przypisać odpowiednią wagę przy generowaniu. O tym będzie o wiele później. Kolejnym zmartwieniem jest to, że aktualnie generowany jest tylko jeden chunk. O tym postaram się napisać w najbliższym czasie.

 

Na dzisiaj to wszystko, GL&HF.

Opublikowano

napisz przy okazji jak uzyskać anty-aliasing w Unity3D

W unity antialiasing można zrobić na dwa sposoby. Pierwszy z nich to ustawienie w Quality Settings antialiasingu na taki jaki sobie życzysz. Druga możliwość to dodanie image effect z antialiasingiem do kamery. Niestety druga opcja jest dostępna tylko w wersji pro.

Opublikowano

Cześć,

 

Drobny refactor. Zauważyłeś pewnie, że w Chunk.cs często korzystam z czegoś takiego:

World.activeWorld.ch_width

Pojawia się to w wielu miejscach.

Można tego uniknąć. Przy zmiennych w tym skrypcie dodaj:

public static ind chunkWidth {
     get { return World.activeWorld.ch_width; }
}

public static ind chunkHeight {
     get { return World.activeWorld.ch_height; }
}

Te dwa gettery ułatwią dalszą pracę.

Zamień teraz wszystkie World.activeWorld.ch_height na chunkHeight, a World.activeWorld.ch_width na chunkWidth;

 

No i widzimy się jutro.

Opublikowano

6. Więcej chunków.

 

Zanim zaczniesz tą część tutoriala, mam do Ciebie jedną prośbę. Jeżeli zrobiłeś refactor z poprzedniej części, odszukacj w Chunk.cs następującą linijkę:

map = new byte[width, width, height]; 

i zmień na:

map = new byte[width, height, width]; 

Mój błąd i niedopatrzenie, za które przepraszam. Poprzednia wersja działała tylko w przypadku, gdy szerokość chunka była taka sama jak jego wysokość. W innym przypadku sypało błędami. Teraz ustawiłem u siebie szerokość na 50, a wysokość na 20.

 

A teraz przejdę do generowania większej liczby chunków.

 

Do Chunk.cs dodaj listę.

public static List<Chunk> chunks = new List<Chunk>();

Na początku metody Start() dodaj do niej aktualny chunk.

chunks.Add (this);

Kolejną niezbędną rzeczą jest metoda, która wyszukuje chunki.

public static Chunk GetChunk(Vector3 position)

Jako parametr przyjmuje Vector3 miejsca w którym znajduje się gracz. Następnie przeszukuje listę chunks:

for (int i = 0; i < chunks.Count; i++)
{

Teraz potrzebuję pozycję chunka z listy:

	Vector3 chunkPos = chunks[i].transform.position;

i sprawdzam, czy pozycja gracza na danej osi x lub z ( na chwilę obecną nie będę układał chunków w pionie), jest mniejsza od pozycji chunka, lub czy pozycja gracza jest większa od pozycji chunka i jego szerokości. Brzmi zawile, ale kod rozjaśni sytuację.

	if ( ( position.x < chunkPos.x) || (position.z < chunkPos.z) ||
	(position.x >= chunkPos.x + width) || (position.z >= chunkPos.z + width)) 

Jeżeli wszystko jest OK to iteruję dalej.

		continue;
	return chunks[i];
}

Jeżeli jednak chunk jest potrzebny, to zwracam ten Chunk, który jest w liście z indeksem .

W przypadku, w którym wyjdę z pętli zwracam null.

return null;
}

W tej części to wszystko co jest związane z Chunk.cs

W razie wątpliwości, tutaj jest cała metoda GetChunk:

 

 

	public static Chunk GetChunk(Vector3 position)
	{
		for (int i = 0; i < chunks.Count; i++)
		{
			Vector3 chunkPos = chunks[i].transform.position;
			if ( ( position.x < chunkPos.x) || (position.z < chunkPos.z) ||
			    (position.x >= chunkPos.x + width) || (position.z >= chunkPos.z + width)) 
				continue;
			return chunks[i];
		}
		return null;
	}

 

 

 

Przejdź do skryptu World.cs

Dodaj do niego dwie zmienne publiczne. Jedna typu float - odpowiadająca za zasięg widzenia. Druga zmienna jest typu Chunk. W niej będzie trzymany prefab chunka.

	public float viewRange = 50;
	public Chunk chunkPrefab;

Żeby utrzymać porzadek w projekcie, stwórz nowy katalog na prefaby. Zmień też w hierarchii nazwę Cube na chunk i stwórz z niego prefab (Przeciągnij do wcześniej stworzonego folderu. Jeżeli wszystko poszło zgodnie z planem, nazwa chunk zmieni swój kolor na niebieski).

 

Zaznacz obiekt, na którym jest skrypt World.cs. O ile nic nie zmieniałeś to znajduje się on na obiekcie Main Camera (w moim przypadku Main Camera jest childem First Person Controllera). Przypisz teraz wartości dla nowych zmiennych. Pamiętaj, aby viewRange był większy od ch_width. Przeciągnij także prefab chunka w miejsce Chunk Prefab.

 

Wróćmy do skryptu.

Dodaj metodę Update(), jeżeli wcześniej ją usuwałeś. Dodaj do niej dwie pętle:

for (float x = transform.position.x - viewRange; x < transform.position.x + viewRange; x+= ch_width)
{
	for (float z = transform.position.z - viewRange; z < transform.position.z + viewRange; z += ch_width)
	{

Tak jak wcześniej wspomniałem: teraz robię tylko chunki na jednym poziomie. Jeżeli je dodawać pionowo, dodaj pętlę z osią y do GetChunk w Chunk.cs oraz w World.cs.

W drugiej pętli robię Vector3 składający się z wartości x oraz z, które później zaokrąglam. Ten Vector3 przekazuję do metody GetChunk. Zaokrąglam je po to, aby w razie konieczności dodawać nowe chunki w równych odstępach.

					Vector3 pos = new Vector3(x, 0, z);
					pos.x = Mathf.Floor (pos.x / (float)ch_width) * ch_width;
					pos.z = Mathf.Floor (pos.z / (float)ch_width) * ch_width;

Sprawdzam teraz co zwróci mi GetChunk:

					Chunk chunk = Chunk.GetChunk(pos);
					if (chunk != null) continue;

Jeżeli nie zwróci null to iteruję dalej, jeżeli nie to tworzę nowy chunk:

					chunk = (Chunk)Instantiate (chunkPrefab, pos, Quaternion.identity);

To tyle ze skryptowania w tej części tutoriala. Po uruchomieniu symulacji powinieneś zobaczyć mniej więcej podobny widok:

U0dX57Q.png

Zauważysz pewnie, że każdy chunk jest taki sam.

 

 

6db0093d0371.jpg

 

 

Jest także mocno odczuwalny lag przy tworzeniu nowych chunków. Podsumowując ten odcinek: Jeżeli wszystko poszło zbyt łatwo to znaczy, że coś jeszcze nie działa.

Jak poradzić sobie z dwoma problemami, które wymieniłem opowiem w następnej części.

 

Całość World.cs:

 

 

using UnityEngine;
using System.Collections;

public class World : MonoBehaviour {

	public int ch_width = 50;
	public int ch_height = 20;
	public int randomSeed = 0;

	public float viewRange = 100;
	public Chunk chunkPrefab;

	public static World activeWorld;

	// inicjalizacja
	void Awake () {
		activeWorld = this;
		if(randomSeed == 0)
			randomSeed = Random.Range(0, int.MaxValue);
	}

	void Update () {
		for (float x = transform.position.x - viewRange; x < transform.position.x + viewRange; x+= ch_width)
		{

				for (float z = transform.position.z - viewRange; z < transform.position.z + viewRange; z += ch_width)
				{
					Vector3 pos = new Vector3(x, 0, z);
					pos.x = Mathf.Floor (pos.x / (float)ch_width) * ch_width;
					pos.z = Mathf.Floor (pos.z / (float)ch_width) * ch_width;

					Chunk chunk = Chunk.GetChunk(pos);
					if (chunk != null) continue;

					chunk = (Chunk)Instantiate (chunkPrefab, pos, Quaternion.identity);

				}

		}
	}

} 

 

 

Opublikowano

6.1. Druga optymalizacja.

 

Najpierw zmniejszymy lag, który występuje podczas dodawania chunków.

Przejdź do metody GenerateMesh() w skrypcie Chunk.cs

	public virtual void GenerateMesh() {

i zamień void na IEnumerator.

 

Następnie w metodzie Start()

GenerateMesh()

"Obuduj" w StartCoroutine.

		StartCoroutine(GenerateMesh());

To powinno zmniejszyć klatkowanie. Drobna uwaga: podczas symulacji w Unity, lag dalej może występować.

Pierwszy problem można uznać za rozwiązany.

 

Teraz trzeba tylko pozbyć się takich samych chunków.

W metodzie Start() w Chunk.cs ustawię seed na ten, który wygenerował się na starcie skryptu World.cs:

	Random.seed = World.activeWorld.randomSeed;

Ustalanie wartości floatów do generowania szumu wygląda aktualnie tak:

	float perlinX = (float)x / 15;

Zmień zmień perlinX, perlinY oraz perlinZ w nasępujący sposób:

float perlinX = Mathf.Abs((float)(x + transform.position.x) / 30);

W skrócie: do każdej zmiennej dodaj transform.position.<oś> oraz zrób z tego wartość bezwzględną za pomocą Mathf.Abs.

 

Pewnie zapytasz: Po co ten Random.Seed?

A ja wtedy odpowiem: Uruchom symulację. Zauważysz, że chunki są "lustrzanym odbiciem" wtedy, kiedy ich pozycja na osi x lub z jest mniejsza niż 0. 

Żeby pozbyć się tego problemu stwórz Vector3 offset linijkę pod ustawiamien Random.seed.

Vector3 offset = new Vector3(Random.value * 9999, Random.value * 9999, Random.value * 9999);

To ma być badassowo (dla purystów języka polskiego: złodupny) losowy Vector3. Dlatego ten seed.

A po co ten offset?

Dodam go do zmiennych perlinX, perlinY i perlinZ.

float perlinX = Mathf.Abs((float)(x + transform.position.x + offset.x) / 30);

Zaktualizowana metoda Start() w Chunk.cs:

 

 

	void Start () {

		chunks.Add (this);

		meshCollider = GetComponent<MeshCollider>();
		meshRenderer = GetComponent<MeshRenderer>();
		meshFilter = GetComponent<MeshFilter>();

		map = new byte[width, height, width];

		Random.seed = World.activeWorld.randomSeed;
		Vector3 offset = new Vector3(Random.value * 10000, Random.value * 10000, Random.value * 10000);

		for(int x = 0; x < width; x++)
		{
			float perlinX = Mathf.Abs((float)(x + transform.position.x + offset.x) / 30);
			for (int y = 0; y < height; y++)
			{
				float perlinY = Mathf.Abs((float)(y + transform.position.y + offset.y) / 50);
				for (int z = 0; z < width; z++)
				{
					float perlinZ = Mathf.Abs((float)(z +transform.position.z + offset.z) / 30);
					float perlin = Noise.Generate(perlinX,perlinY,perlinZ);

					perlin += (10f - (float)y) / 10;

					if( perlin > 0.05f)
						map[x,y,z] = 1;
				}
			}
		}
		StartCoroutine(GenerateMesh());
	} 

 

 

 

Przypominam o hardcode, który możesz zmieniać dowolnie. Ja w tej wersji stawiam kloc kiedy perlin jest większy od 0.05 i bardziej "ściskam" perlinY.

Opublikowano

Wszystko ok.

Jedna uwaga ode mnie.

Jak piszesz coś takiego.

Przejdź do metody GenerateMesh() w skrypcie Chunk.cs

	public virtual void GenerateMesh() {

i zamień void na IEnumerator.

 

Następnie w metodzie Start()

GenerateMesh()

"Obuduj" w StartCoroutine.

		StartCoroutine(GenerateMesh());

To powinno zmniejszyć klatkowanie.

to wyjaśniaj jak najlepiej jak to działa i dlaczego. Ja nie wiem, a z chęcią bym się dowiedział.

Ogólnie to wszystko mógłbyś lepiej tłumaczyć trochę.

I dodaj obrazek już z poprawnie wygenerowanymi chunkami

Opublikowano

Wszystko ok.

Jedna uwaga ode mnie.

Jak piszesz coś takiego.

to wyjaśniaj jak najlepiej jak to działa i dlaczego. Ja nie wiem, a z chęcią bym się dowiedział.

Ogólnie to wszystko mógłbyś lepiej tłumaczyć trochę.

I dodaj obrazek już z poprawnie wygenerowanymi chunkami

Dzięki za zwrócenie uwagi. Na przyszłość postaram się tłumaczyć dokładniej. 

Co do IEnumeratora:

W poprzedniej wersji, używałem void. To znaczy, że wszystko działo się w tym samym momencie. Przy użyciu IEnumeratora, to co jest w GenerateMesh pójdzie w tło. Dla przykładu:

 

IEnumerator SpawnEnemy(float time, Enemy monster)
{
     Spawn(monster);
     yield return new WaitForSeconds(time);
     Spawn(monster);
     // ...
}

Wstrzyma wykonanie na podany czas przez użycie instrukcji yield. Wykona się to asynchronicznie w stosunku do tego co dzieje się na mapie. StartCoroutine to nic innego jak sposób wywołania IEnumeratora. Chciałbym zaznaczyć też, że IEnumerator GenerateMesh jest raczej tymczasowym rozwiązaniem. Kolejne optymalizacje obejmą też problem z wyświetlaniem dolnej i bocznych ścian chunka.

 

No i screen:

Nc7QZHX.png

 

Moja prośba:

Bierzcie przykład z @Sopelek997 i pytajcie. Z racji tego, że to mój pierwszy większy tutorial nie jestem w stanie uniknąć niektórych błędów. To pomoże wszystkim.

Opublikowano

Jest jakiś zamiennik SourceTree na XP?

Jakaś starsza wersja githuba albo konsola z komendami do gita.

Opublikowano

Jakby co: Ja bym w następnym Poscie-Tutorialu dodał "biom", chodzi o to że ten wygenerowany teren co ty Teraz masz jest strasznie górzysty, a przydało by się trochę "nizin" czyli jako-takiego płaskiego terenu

Opublikowano

Jakby co: Ja bym w następnym Poscie-Tutorialu dodał "biom", chodzi o to że ten wygenerowany teren co ty Teraz masz jest strasznie górzysty, a przydało by się trochę "nizin" czyli jako-takiego płaskiego terenu

Biomy będą nieco później. Zanim do tego dojdzie będę jeszcze modyfikował teren w taki sposób, żeby powstawały podziemne jaskinie. Jeżeli teraz chcesz bardziej spłaszczony teren to możesz dzielić perlinY przez większa liczbę.

Opublikowano

Cześć,

 

Ze złych wieści: Niestety jestem zmuszony przenieść premierę dzisiejszego odcinka na jutro. 

Dlaczego? Już wyjaśniam...

Z dobrych wieści: Wystartował game jam

http://thejam.pl/

w którym biorę udział. Niestety cały dzisiejszy dzień zejdzie mojej ekipie na prototypy.

 

Zachęcam Cię do zapoznania się z tematem jamu (który nie jest bardzo wymagający nawet dla początkujących) i wzięcia w nim udziału. Kto wie, może to Ty odbierzesz główną nagrodę w Poznaniu. GL&HF.

Opublikowano

Z dobrych wieści: Wystartował game jam

http://thejam.pl/

 

Jestem ciekaw jak wam to wyjdzie, ElectroBody to jedna z moich ulubionych gier pod DOSa. Życzę Wam powodzenia.

Opublikowano

Gdzieś na stopro się pochwalę, ale nie prędko i raczej nie w temacie z tutorialem :D

Opublikowano

Cześć, Dzisiaj jeden krótki wpis dotyczący tekstur. Weekend kurczy się niemiłosiernie, a pomysłów na platformówki mamy za dużo :D Zacznij od stworzenia folderu na tekstury oraz folderu ma materiały. Aktualnie korzystam z tego zestawu tekstur:

42699140403751024822.png

Nie mam pojęcia jak wygląda licencja na te obrazki, ale tymczasowo uznajmy że jest w celach edukacyjnych. Gdybyś znał kogoś, lub sam potrafił stworzyć prosty i równy spritesheet 512x512 i zechciał podzielić się nim na potrzeby tutoriala to będę bardzo wdzięczny.

Wrzuć to do folderu z teksturami.

W inspektorze wybierz Wrap Mode - Clamp, Filter Mode - Point. Max size 512.

Następnie stwórz materiał. Prawy klik -> create -> material. Shader to najzwyklejszy Diffuse.

Następnie wrzuć przygotowaną przed chwilą teksturę na materiał. Przejdź teraz do folderu z prefabem chunka.

Kiedy zaznaczysz ten prefab, w inspektorze powinien znajdować się między innymi Mesh Renderer. Rozwiń w nim Materials, ustaw size na 1 i dodaj materiał, który stworzyłeś.

 

Play!

 

No w sumie coś tam się dzieje, ale tak sobie.

Przejdźmy więc do kodu.

W skrypcie Chunk.cs odszukaj metodę GenerateFace. Skupimy się nad czterema linijkami, w których dodaję koordynaty do listy uvs. Dodaj nad nimi dwa Vector2:

Vector2 uv_width = new Vector2(0.25f, 0.25f); Vector2 uv_corner = new Vector2(0.25f,0.75f);

Pierwszy z nich odpowiada za szerokość nakładanej tekstury. Skąd te liczby? Obrazek, z którego korzystam składa się z 4x4 oddzielnych tekstur. Ponieważ mapowanie, które robię przyjmuje wartości od 0 do 1, to prosta arytmetyka dzieląca 1 na 4 daje 0.25. Drugi Vector2 przyjmuje pozycję na teksturze, od której ma zacząć mapowanie. Ja wybrałem trawkę. Pierwsza wartość 0.25 to "drugi klocek od lewej", a 0.75 to "trzeci klocek od dołu". W skrócie x i y.

Kolejną rzeczą jest zmodyfikowanie poniższych czterech linijek zaczynających się od uvs.Add(...); W pierwszej linijce zamiast (0,0) dodaję uv_corner.

uvs.Add (uv_corner);

Następnie zgodnie ze ruchem wskazówek zegara dodaję do tej tablicy kolejne współrzędne.

uvs.Add(new Vector2 (uv_corner.x, uv_corner.y + uv_width.y));

"Lewy, górny róg". Ten sam punkt x, a punkt y zwiększony o rozmiar "małej" tekstury.

uvs.Add(new Vector2 (uv_corner.x + uv_width.x, uv_corner.y + uv_width.y));

"Prawy, górny róg".

uvs.Add(new Vector2 (uv_corner.x + uv_width.x, uv_corner.y));

Last but not least - prawy dolny :D

Zmodyfikowana metoda GenerateFace:

public virtual void GenerateFace(byte block, Vector3 corner, Vector3 up, Vector3 right, bool rev, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
    {
        int index = verts.Count;
        verts.Add (corner);
        verts.Add (corner + up);
        verts.Add (corner + up + right);
        verts.Add (corner + right);

        Vector2 uv_width = new Vector2(0.25f, 0.25f);
        Vector2 uv_corner = new Vector2(0.25f,0.75f);

        uv_corner.x += (float)(block - 1) / 4;

        uvs.Add (uv_corner);
        uvs.Add(new Vector2 (uv_corner.x, uv_corner.y + uv_width.y));
        uvs.Add(new Vector2 (uv_corner.x + uv_width.x, uv_corner.y + uv_width.y));
        uvs.Add(new Vector2 (uv_corner.x + uv_width.x, uv_corner.y));

        if(rev)
        {
            tris.Add (index + 0);
            tris.Add (index + 1);
            tris.Add (index + 2);
            tris.Add (index + 2);
            tris.Add (index + 3);
            tris.Add (index + 0);
        } else {
            tris.Add (index + 1);
            tris.Add (index + 0);
            tris.Add (index + 2);
            tris.Add (index + 3);
            tris.Add (index + 2);
            tris.Add (index + 0);
        }
    }

 

 

Rosnąca trawa:

71857140403751024822.png

Z jakim problemem zostawiam Cię teraz? Zauważalne linie pomiędzy blokami i dziwny kolor kiedy patrzysz na dalsze klocki "pod światło". To muszę sprawdzić, czy czasem nie jest winą shadera, czy moich obliczeń albo krzywej tekstury. Co w następnej części? Kolejny refactor. Zauważ, że w metodzie Start() w skrypccie Chunk.cs są wykonywane obliczenia, które spokojnie można upchnąć w osobną metodę. Dodatkowo pozbędę się dziur, przez które można spadać w nieskończoność :(

Zarchiwizowany

Ten temat przebywa obecnie w archiwum. Dodawanie nowych odpowiedzi zostało zablokowane.

×
×
  • Dodaj nową pozycję...